using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace PhpDocBlockGenerator.Model.Php
{
    /// <summary>
    /// class for a php method boy and definition
    /// </summary>
    public class PhpMethod : IPhpClassElements
    {
        /// <summary>
        /// the whole file as an generic list
        /// </summary>
        private List<String> _fileArr;

        /// <summary>
        /// beginning position of the function head in the lines array
        /// </summary>
        public int BeginningLine { get; private set; }

        /// <summary>
        /// ending position of the function in the lines array
        /// </summary>
        public int EndingLine { get; private set; }

        /// <summary>
        /// The Body of the function
        /// </summary>
        private String _methodBody = String.Empty;
        public String MethodBody
        {
            get { return _methodBody; }
            set { _methodBody = value; }
        }

        /// <summary>
        /// The Name of the function
        /// </summary>
        private String _methodName = String.Empty;
        public String MethodName
        {
            get { return _methodName; }
            set { _methodName = value; }
        }

        /// <summary>
        /// the docblock created by the program itself
        /// </summary>
        public PhpMethodDocBlock GeneratedDocBlock { get; set; }

        /// <summary>
        /// the docblock merged from the user and generated one
        /// </summary>
        public PhpMethodDocBlock MergedDocBlock { get; set; }

        /// <summary>
        /// The Name of the Class
        /// </summary>
        public string ParentPhpClassName { get; set; }

        /// <summary>
        /// The Docblock created by the User
        /// </summary>
        public PhpMethodDocBlock UserDocBlock { get; set; }

        /// <summary>
        /// indent to use for doc block (taken from line which contains "function")
        /// </summary>
        private String _indent = String.Empty;

        /// <summary>
        /// the options container
        /// </summary>
        private Options _options;

        /// <summary>
        /// Ctor
        /// </summary>
        /// <param name="fileArr">list of file lines which contains the method data</param>
        /// <param name="beginningLineOfMethod">beginning line number of the function-keyword in
        /// the list</param>
        /// <param name="methodName">parsed name of the method</param>
        /// <param name="options"></param>
        public PhpMethod(List<String> fileArr, int beginningLineOfMethod, String methodName, Options options)
        {
            _methodName = methodName;
            _fileArr = fileArr;
            BeginningLine = beginningLineOfMethod;
            _options = options;

            _GenerateDocBlock();
            _ParseMethodDocBlock();
        }


        /// <summary>
        /// compares and merges the generated method doc block to the existing one
        /// and writes the merged data back to the list of file lines
        /// </summary>
        public void CompareAndMerge()
        {
            if (null == UserDocBlock)
                MergedDocBlock = GeneratedDocBlock;
            else
                MergedDocBlock = GeneratedDocBlock.Compare(UserDocBlock);
        }

        /// <summary>
        /// writes the changed lines back to the list of file lines
        /// </summary>
        /// <param name="allFileLines">complete file as list of lines</param>
        /// <param name="writingPos">position to begin writing in the complete list of file lines</param>
        /// <param name="writingOffset">offset of earlier written lines to regard for</param>
        public void WriteToListOfFileLines(List<String> allFileLines, int writingPos, ref int writingOffset)
        {
            //TODO: need own exception type here
            //if no merged doc block exists, throw exception
            if (null == MergedDocBlock)
                throw new Exception();

            //calc new position of all lines (due to earlier writing operations in the list of lines)
            int newPos = BeginningLine + writingPos;

            //get doc block as list of lines
            List<String> docBlockAsList = MergedDocBlock.ToList(_indent);

            //calc number of lines of old doc block (if exists)
            int numberOfLinesOldBlock = 0;
            if (null != UserDocBlock &&
                -1 != UserDocBlock.DocBlockBegin && -1 != UserDocBlock.DocBlockEnd)
            {
                numberOfLinesOldBlock =
                    UserDocBlock.DocBlockEnd - UserDocBlock.DocBlockBegin + 1;

                //delete space-lines between docblock and method
                int spaceLines = BeginningLine - UserDocBlock.DocBlockEnd - 1;
                if (spaceLines > 0)
                {
                    allFileLines.RemoveRange(newPos - spaceLines, spaceLines);
                    newPos -= spaceLines;
                    writingOffset -= spaceLines;
                }
            }

            //calc difference between old and new doc block
            int diffNumberOfLines = docBlockAsList.Count - numberOfLinesOldBlock;
            int newPosOldDocBlock = newPos - numberOfLinesOldBlock;

            //remove old block and add new one
            allFileLines.RemoveRange(newPosOldDocBlock, numberOfLinesOldBlock);
            allFileLines.InsertRange(newPosOldDocBlock, docBlockAsList);

            writingOffset += diffNumberOfLines;
        }


        /// <summary>
        /// Creates a new Docblock-object for the parsed values
        /// </summary>
        private void _GenerateDocBlock()
        {
            GeneratedDocBlock = new PhpMethodDocBlock(_options);
            GeneratedDocBlock.ShortDescription.Description = "<Description of " + _methodName + ">";

            _GetIndent();
            _SearchForMethodParams();
            if (!_IsAbstractMethod())
            {
                _SearchForMethodBody();
                _SearchForMethodReturnType();
                _SearchForMethodThrows();
            }
        }

        /// <summary>
        /// checks whether this method is abstract or not (ends with ;)
        /// </summary>
        /// <returns></returns>
        private bool _IsAbstractMethod()
        {
            // return true if it matches a regular one line abstract function
            if (PhpRegex.RegSearchAbstractMethod.IsMatch(_fileArr[BeginningLine]))
                return true;

            // iterate through the file array and search for opening curly brace or semicolon
            int i = BeginningLine;
            while (i < _fileArr.Count)
            {
                if (_fileArr[i].Split(new[] {'{'}).Length > 1)
                    return false;

                if (_fileArr[i].Split(new[] {';'}).Length > 1)
                    return true;
                ++i;
            }

            return false;
        }

        /// <summary>
        /// calculates the indent for the doc block (takes it from the line which contains
        /// the function-keyword)
        /// </summary>
        private void _GetIndent()
        {
            if (-1 != BeginningLine)
            {
                Match indentMatch = PhpRegex.RegSearchIndent.Match(_fileArr[BeginningLine]);

                if (indentMatch.Success)
                    _indent = indentMatch.Value;
            }
        }

        /// <summary>
        /// checks the datatype of a single return
        /// </summary>
        /// <param name="match"></param>
        /// <returns></returns>
        private PhpDataType _GetReturnDataType(Match match)
        {
            //add all datatype regexes to a list
            var listRegex = new List<Regex>
                {
                    PhpRegex.RegSearchThis,
                    PhpRegex.RegSearchInt,
                    PhpRegex.RegSearchFloat,
                    PhpRegex.RegSearchBoolean,
                    PhpRegex.RegSearchString,
                    PhpRegex.RegSearchArray,
                    PhpRegex.RegSearchNull
                };

            //match the given matchstring for a datatype
            //if sth found, return the corresponding data type
            for (int i = 0; i < listRegex.Count; ++i)
            {
                if (listRegex[i].Matches(match.Value).Count > 0)
                    return new PhpDataType((PhpDataTypes)i);
            }

            //nothing was found, so return unknown-type
            return new PhpDataType(PhpDataTypes.Mixed);
        }

        /// <summary>
        /// checks for return types in the function body
        /// </summary>
        /// <param name="returns"></param>
        /// <returns></returns>
        private PhpDataType _GetReturnType(MatchCollection returns)
        {
            PhpDataType rType = new PhpDataType();


            //loop over all given matches of return-statements of the method body
            foreach (Match match in returns)
            {
                //extract return type of the current match
                PhpDataType currRt = _GetReturnDataType(match);

                //if it's the first return-statement then save its type
                if (rType.Equals(PhpDataTypes.None))
                    rType = currRt;
                //otherwise if the current return-statement is unknown (e.g. user type)
                //then the entire return type must also be unknown -> exit loop
                else if (currRt.Equals(PhpDataTypes.Mixed))
                {
                    rType = new PhpDataType(PhpDataTypes.Mixed);
                    break;
                }
                //otherwise if the saved return type isn't equal to the current type
                //and the entire return type isn't unknown (I think this second condition
                //isn't really necessary, because we exit the loop earlier when type was
                //unknown)
                else if (!rType.Equals(currRt) && !rType.Equals(PhpDataTypes.Mixed))
                {
                    //if entire type is already mixed then save current type as subtype
                    if (!rType.Equals(PhpDataTypes.Mixed))
                    {
                        //set it to mixed and save current and previous type
                        //as subtypes
                        PhpDataType temp = new PhpDataType(PhpDataTypes.Mixed);

                        rType = temp;
                    }
                }
            }

            return rType;
        }

        /// <summary>
        /// parses the file lines for an existing doc block
        /// </summary>
        private void _ParseMethodDocBlock()
        {
            int begin, end;
            UserDocBlock = new PhpMethodDocBlock(_options) {IsUserBlock = true};

            //find out begin and end of the doc block (if there is one)
            StaticMethods.ParseDocBlockPos(_fileArr, BeginningLine, out begin, out end);

            //if doc block exists
            if (begin != -1 && end != -1)
            {
                //extract it from file lines array, parse it and save position
                List<String> docBlockLines = _fileArr.GetRange(begin, end - begin + 1);
                UserDocBlock.Parse(docBlockLines);
                UserDocBlock.DocBlockBegin = begin;
                UserDocBlock.DocBlockEnd = end;
            }
        }

        /// <summary>
        /// searches the file lines for the complete method body
        /// </summary>
        private void _SearchForMethodBody()
        {
            String completeMethodBlock = String.Empty;
            int i = 0, curlyCount = 0, openCurlyWithoutClosingCurlyCount = 0;

            // we have to find at least one pair of braces and then iterate until the number
            // of open curly braces and closing curly braces doesn't differ
            while (!(curlyCount > 0 && openCurlyWithoutClosingCurlyCount == 0))
            {
                // number of open curly braces on that line
                int openCurlyCountLine = _fileArr.ElementAt(BeginningLine + i).Split(new char[] {'{'}).Length - 1;
                // number of closing curly braces on that line
                int closingCurlyCountLine = _fileArr.ElementAt(BeginningLine + i).Split(new char[] { '}' }).Length - 1;
                if (openCurlyCountLine > 0)
                    ++curlyCount;
                openCurlyWithoutClosingCurlyCount += openCurlyCountLine - closingCurlyCountLine;

                // add to string
                if (i > 0)
                    completeMethodBlock += _fileArr.ElementAt(BeginningLine + i);
                ++i;
            }

            EndingLine = BeginningLine + i;

            _methodBody = completeMethodBlock;
        }

        /// <summary>
        /// searches the methods body for possible parameters
        /// </summary>
        private void _SearchForMethodParams()
        {
            MatchCollection regOcbResult;
            int j = 0;
            String Params = String.Empty;


            //search sourcecode for an opening curly brace
            do
            {
                regOcbResult = PhpRegex.RegSearchOpeningCurlyBraceOrSemicolon.Matches(_fileArr[BeginningLine + j]);
                ++j;
            } while (0 == regOcbResult.Count);


            //concat all lines which contain parameters
            for (int k = 0; k < j; ++k)
                Params += _fileArr[BeginningLine + k];

            // extract all parameters
            MatchCollection regParamsResult = PhpRegex.RegSearchParamsAsBlock.Matches(Params);
                //PhpRegex.RegSearchParams.Matches(Params);

            //loop over all extracted params
            foreach (Match paramMatch in regParamsResult)
            {
                //extract param type and name
                MatchCollection regParamTypeAndNameResult = PhpRegex.RegSearchParamTypeAndName.Matches(paramMatch.Value);

                //next: add parameter to param list of the doc block to generate

                //if there is a type specified for this parameter
                if(regParamTypeAndNameResult.Count > 1)
                {
                    GeneratedDocBlock.Params.Add(new DocBlockPropertyParam(_options)
                    {
                        VariableName = regParamTypeAndNameResult[1].Value,
                        ObjectType = DocBlockPropertyTypes.Param,
                        VariableDataType = PhpDataType.CreateFromString(regParamTypeAndNameResult[0].Value)
                    });
                }
                //otherwise there is only a parameter name
                //simply ignore no matches
                else if(1 == regParamTypeAndNameResult.Count)
                {
                    GeneratedDocBlock.Params.Add(new DocBlockPropertyParam(_options)
                    {
                        VariableName = regParamTypeAndNameResult[0].Value,
                        ObjectType = DocBlockPropertyTypes.Param,
                        VariableDataType = new PhpDataType { Type = PhpDataTypes.Mixed }
                    });
                }
            }
        }

        /// <summary>
        /// searches the methods body for possible return types
        /// </summary>
        private void _SearchForMethodReturnType()
        {
            MatchCollection regReturnResult = PhpRegex.RegSearchPropReturn.Matches(_methodBody);
            GeneratedDocBlock.Return.Add(new DocBlockPropertyReturn(_options)
                {
                    VariableDataType = _GetReturnType(regReturnResult)
                });
        }

        /// <summary>
        /// searches the methods body for possible throwed exceptions
        /// </summary>
        private void _SearchForMethodThrows()
        {
            MatchCollection regThrowNewResult = PhpRegex.RegSearchThrowNew.Matches(_methodBody);


            foreach (Match throwNewMatch in regThrowNewResult)
            {
                DocBlockPropertyException exception = new DocBlockPropertyException(_options)
                    { ObjectType = DocBlockPropertyTypes.Throws };
                exception.VariableDataType.TypeFromString = throwNewMatch.Value;

                GeneratedDocBlock.Throws.Add(exception);
            }

            GeneratedDocBlock.Throws = (from exc in GeneratedDocBlock.Throws
                                              select exc).Distinct(new DocBlockPropertyExceptionComparer())
                                              .ToList();
        }
    }
}
